// @ts-strict-ignore
import type { SettingParams } from '@/types/settingTypes'
import { app } from '../../scripts/app'
import {
  LGraphCanvas,
  LGraphNode,
  LGraphGroup,
  LiteGraph
} from '@comfyorg/litegraph'

// Shift + drag/resize to snap to grid

/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
function roundVectorToGrid(vec) {
  vec[0] =
    LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE)
  vec[1] =
    LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE)
  return vec
}

app.registerExtension({
  name: 'Comfy.SnapToGrid',
  init() {
    // Add setting to control grid size
    app.ui.settings.addSetting({
      id: 'Comfy.SnapToGrid.GridSize',
      category: ['Comfy', 'Graph', 'GridSize'],
      name: 'Snap to grid size',
      type: 'slider',
      attrs: {
        min: 1,
        max: 500
      },
      tooltip:
        'When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.',
      defaultValue: LiteGraph.CANVAS_GRID_SIZE,
      onChange(value) {
        LiteGraph.CANVAS_GRID_SIZE = +value || 10
      }
    })
    // Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.
    // Using a new setting id can cause existing users to lose their existing settings.
    const alwaysSnapToGrid = app.ui.settings.addSetting({
      id: 'pysssss.SnapToGrid',
      category: ['Comfy', 'Graph', 'AlwaysSnapToGrid'],
      name: 'Always snap to grid',
      type: 'boolean',
      defaultValue: false,
      versionAdded: '1.3.13'
    } as SettingParams)

    const shouldSnapToGrid = () => app.shiftDown || alwaysSnapToGrid.value

    // After moving a node, if the shift key is down align it to grid
    const onNodeMoved = app.canvas.onNodeMoved
    app.canvas.onNodeMoved = function (node) {
      const r = onNodeMoved?.apply(this, arguments)

      if (shouldSnapToGrid()) {
        // Ensure all selected nodes are realigned
        for (const id in this.selected_nodes) {
          this.selected_nodes[id].alignToGrid()
        }
      }

      return r
    }

    // When a node is added, add a resize handler to it so we can fix align the size with the grid
    const onNodeAdded = app.graph.onNodeAdded
    app.graph.onNodeAdded = function (node) {
      const onResize = node.onResize
      node.onResize = function () {
        if (shouldSnapToGrid()) {
          roundVectorToGrid(node.size)
        }
        return onResize?.apply(this, arguments)
      }
      return onNodeAdded?.apply(this, arguments)
    }

    // Draw a preview of where the node will go if holding shift and the node is selected
    const origDrawNode = LGraphCanvas.prototype.drawNode
    LGraphCanvas.prototype.drawNode = function (node, ctx) {
      if (
        shouldSnapToGrid() &&
        this.node_dragged &&
        node.id in this.selected_nodes
      ) {
        const [x, y] = roundVectorToGrid([...node.pos])
        const shiftX = x - node.pos[0]
        let shiftY = y - node.pos[1]

        let w, h
        if (node.flags.collapsed) {
          w = node._collapsed_width
          h = LiteGraph.NODE_TITLE_HEIGHT
          shiftY -= LiteGraph.NODE_TITLE_HEIGHT
        } else {
          w = node.size[0]
          h = node.size[1]
          // @ts-expect-error
          let titleMode = node.constructor.title_mode
          if (
            titleMode !== LiteGraph.TRANSPARENT_TITLE &&
            titleMode !== LiteGraph.NO_TITLE
          ) {
            h += LiteGraph.NODE_TITLE_HEIGHT
            shiftY -= LiteGraph.NODE_TITLE_HEIGHT
          }
        }
        const f = ctx.fillStyle
        ctx.fillStyle = 'rgba(100, 100, 100, 0.5)'
        ctx.fillRect(shiftX, shiftY, w, h)
        ctx.fillStyle = f
      }

      return origDrawNode.apply(this, arguments)
    }

    /**
     * The currently moving, selected group only. Set after the `selected_group` has actually started
     * moving.
     */
    let selectedAndMovingGroup: LGraphGroup | null = null

    /**
     * Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
     * below) as well as handle the last move call from LiteGraph's `processMouseUp`.
     */
    const groupMove = LGraphGroup.prototype.move
    LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
      const v = groupMove.apply(this, arguments)
      // When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
      // too eagerly and we don't want to behave like we're moving until we get a delta.
      if (
        !selectedAndMovingGroup &&
        app.canvas.selected_group === this &&
        (deltax || deltay)
      ) {
        selectedAndMovingGroup = this
      }

      // LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
      // to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
      // has been set to `false`. Essentially, this check here is the equivalent to calling an
      // `LGraphGroup.prototype.onNodeMoved` if it had existed.
      if (app.canvas.last_mouse_dragging === false && shouldSnapToGrid()) {
        // After moving a group (while shouldSnapToGrid()), snap all the child nodes and, finally,
        //  align the group itself.
        this.recomputeInsideNodes()
        for (const node of this.nodes) {
          node.alignToGrid()
        }
        LGraphNode.prototype.alignToGrid.apply(this)
      }
      return v
    }

    /**
     * Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
     * drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
     * both.
     */
    const drawGroups = LGraphCanvas.prototype.drawGroups
    LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
      if (this.selected_group && shouldSnapToGrid()) {
        if (this.selected_group_resizing) {
          roundVectorToGrid(this.selected_group.size)
        } else if (selectedAndMovingGroup) {
          const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos])
          const f = ctx.fillStyle
          const s = ctx.strokeStyle
          ctx.fillStyle = 'rgba(100, 100, 100, 0.33)'
          ctx.strokeStyle = 'rgba(100, 100, 100, 0.66)'
          ctx.rect(x, y, ...(selectedAndMovingGroup.size as [number, number]))
          ctx.fill()
          ctx.stroke()
          ctx.fillStyle = f
          ctx.strokeStyle = s
        }
      } else if (!this.selected_group) {
        selectedAndMovingGroup = null
      }
      return drawGroups.apply(this, arguments)
    }

    /** Handles adding a group in a snapping-enabled state. */
    const onGroupAdd = LGraphCanvas.onGroupAdd
    LGraphCanvas.onGroupAdd = function () {
      const v = onGroupAdd.apply(app.canvas, arguments)
      if (shouldSnapToGrid()) {
        const lastGroup = app.graph.groups[app.graph.groups.length - 1]
        if (lastGroup) {
          roundVectorToGrid(lastGroup.pos)
          roundVectorToGrid(lastGroup.size)
        }
      }
      return v
    }
  }
})
